<template>
    <div class="piano" :style="{ '--white-notes-count': whiteNotesCount, width, height }">
        <div
            v-for="note in notes"
            :key="note.value"
            class="note"
            :class="[note.color, `${note.color}-${note.state}`]"
            @click="emits('click', note.value, note.sign, note.color)"
            @mousedown="mouseDownToAttack(note)"
            @mouseenter="mouseEnterToAttack(note, $event)"
            @mouseup="mouseUpToRelease(note, $event)"
            @mouseleave="mouseLeaveToRelease(note, $event)"
        >
            <div class="sign">{{ note.sign }}</div>
            <slot :value="note.value" :sign="note.sign" :color="note.color"></slot>
        </div>
    </div>
</template>

<script setup lang="ts">
import { type PropType, ref, watch, computed } from "vue";
import * as Tone from "tone";

/**
 * 音符信息
 */
interface NoteInformation {
    // Midi 值
    value: number;
    // 音符符号
    sign: string;
    // 音符颜色
    color: "white" | "black";
    // 是否正在播放
    state: "mute" | "playing";
}

const props = defineProps({
    octave: {
        type: Object as PropType<[number, number]>,
        default: () => [3, 5],
    },
    width: {
        type: String,
        default: "100%",
    },
    height: {
        type: String,
        default: "100%",
    },
    sounded: {
        type: Boolean,
        default: true,
    },
});

const emits = defineEmits<{ (event: "click", value: number, sign: string, color: "white" | "black"): void }>();

// 合成器
const synth = new Tone.PolySynth().toDestination();

// 一个八度音符的颜色
const colors: ("white" | "black")[] = [
    "white",
    "black",
    "white",
    "black",
    "white",
    "white",
    "black",
    "white",
    "black",
    "white",
    "black",
    "white",
];

// 音符名称
const signs = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];

// 八度数量
const octaveCount = computed(() => Math.max(0, props.octave[1] - props.octave[0] + 1));

// 白色音符数量
const whiteNotesCount = computed(() => 7 * octaveCount.value);

// 音符信息数组
const notes = ref<NoteInformation[]>([]);
watch(
    () => props.octave,
    ([first, last]) => {
        // 先保证不在播放
        synth.releaseAll();

        // 初始化音符信息
        const res: NoteInformation[] = [];
        [first, last] = [12 * first, 12 * last];
        for (let i = first; i <= last; ++i) {
            res.push({
                value: i,
                sign: signs[i % 12] + Math.floor(i / 12).toString(),
                color: colors[i % 12],
                state: "mute",
            });
        }

        // 设置音符信息
        notes.value = res;
    },
    {
        immediate: true,
    },
);

/**
 * 按下鼠标，触发音符
 * @param note 当前音符信息
 */
function mouseDownToAttack(note: NoteInformation) {
    note.state = "playing";

    // 不允许发声
    // 则不继续执行
    if (!props.sounded) {
        return;
    }

    // 发声
    synth.triggerAttack(note.sign);
}

/**
 * 鼠标进入，尝试触发音符
 * @param note 当前音符信息
 * @param event 鼠标事件
 */
function mouseEnterToAttack(note: NoteInformation, event: MouseEvent) {
    // 不处于按下状态
    if (event.buttons === 0) {
        return;
    }

    note.state = "playing";

    // 不允许发声
    // 则不继续执行
    if (!props.sounded) {
        return;
    }

    // 发声
    synth.triggerAttack(note.sign);
}

/**
 * 鼠标释放，尝试释放音符
 * @param note 当前音符信息
 * @param event 鼠标事件
 */
function mouseUpToRelease(note: NoteInformation, event: MouseEvent) {
    // 处于按下状态
    if (event.buttons !== 0) {
        return;
    }

    // 释放
    note.state = "mute";
    synth.triggerRelease(note.sign);
}

/**
 * 鼠标移出，尝试释放音符
 * @param note 当前音符信息
 * @param event 鼠标事件
 */
function mouseLeaveToRelease(note: NoteInformation, event: MouseEvent) {
    // 不处于按下状态
    if (event.buttons === 0) {
        return;
    }

    // 释放
    note.state = "mute";
    synth.triggerRelease(note.sign);
}
</script>

<style scoped lang="scss">
.piano {
    --white-notes-count: 7;
    --white-note-width: calc(100% / var(--white-notes-count));
    --black-note-width: calc(0.5 * var(--white-note-width));

    position: relative;

    width: 100%;
    height: 100%;
    box-sizing: border-box;

    top: -48px;
    margin-top: 96px;
    border-left: transparent 1px solid;
    border-right: transparent 1px solid;

    .note {
        float: left;
        overflow: visible;
        display: flex;
        flex-wrap: wrap;
        justify-content: center;

        box-sizing: border-box;
        position: relative;

        transition: all 0.1s ease;

        .sign {
            user-select: none;
            position: absolute;
            left: 50%;
            font-size: 20px;
            font-weight: bold;
            text-align: center;
            color: rgb(var(--v-theme-on-surface));
        }

        &:first-child {
            border-radius: 5px 0 5px 5px;
        }

        &:last-child {
            border-radius: 0 5px 5px 5px;
        }
    }

    .white {
        z-index: 1;
        width: var(--white-note-width);
        height: 100%;
        align-items: flex-end;

        border-radius: 0 0 5px 5px;

        .sign {
            bottom: -16px;
            transform: translate(-50%, 50%);
        }
    }

    .white-mute {
        border-left: 1px solid #bbb;
        border-bottom: 1px solid #bbb;
        box-shadow:
            -1px 0 0 rgba(255, 255, 255, 0.8) inset,
            0 0 5px #ccc inset,
            0 0 3px rgba(0, 0, 0, 0.2);
        background: linear-gradient(to bottom, #eee 0%, #fff 100%);
    }

    .white-playing {
        border-top: 1px solid #777;
        border-left: 1px solid #999;
        border-bottom: 1px solid #999;
        box-shadow:
            2px 0 3px rgba(0, 0, 0, 0.1) inset,
            -5px 5px 20px rgba(0, 0, 0, 0.2) inset,
            0 0 3px rgba(0, 0, 0, 0.2);
        background: linear-gradient(to bottom, #fff 0%, #e9e9e9 100%);
    }

    .black {
        z-index: 2;
        width: var(--black-note-width);
        height: 62%;
        align-items: flex-start;

        position: relative;
        left: calc(var(--black-note-width) / (-2));
        margin-right: calc(-1 * var(--black-note-width));

        border: 1px solid #000;
        border-radius: 0 0 3px 3px;

        .sign {
            top: -16px;
            transform: translate(-50%, -50%);
        }
    }

    .black-mute {
        box-shadow:
            -1px -1px 2px rgba(255, 255, 255, 0.2) inset,
            0 -5px 2px 3px rgba(0, 0, 0, 0.6) inset,
            0 2px 4px rgba(0, 0, 0, 0.5);
        background: linear-gradient(45deg, #222 0%, #555 100%);
    }

    .black-playing {
        box-shadow:
            -1px -1px 2px rgba(255, 255, 255, 0.2) inset,
            0 -2px 2px 3px rgba(0, 0, 0, 0.6) inset,
            0 1px 2px rgba(0, 0, 0, 0.5);
        background: linear-gradient(to right, #444 0%, #222 100%);
    }
}
</style>
